#!/usr/bin/python
# -*- coding: utf8 -*-
"""\
Poker logic
"""
from itertools import islice
import random


class Card(object):
    __VALUE_SYMBOL = {10: 'T', 11: 'J', 12: 'Q', 13: 'K', 14: 'A'}
    
    def __init__(self, value, suit):
        self.suit = suit
        self.value = value
    def __str__(self):
        return "%s%s" % (self.suit, 
                         Card.__VALUE_SYMBOL.get(self.value, self.value))
    def __repr__(self):
        return "Card(%s, '%s')" % (self.value, self.suit)
    def __cmp__(self, other):
        """Compare card value, ignore suits """
        return cmp(self.value, other.value)
    @staticmethod
    def from_string(code):
        """Translate string representation to Card instance 
        >>> Card.from_string('H2')
        Card(2, 'H')
        >>> Card.from_string(u'HQ')
        Card(12, 'H')
        >>>
        """
        try:
            return Card(int(code[1]), code[0])
        except ValueError:
            for (k, v) in Card.__VALUE_SYMBOL.iteritems():
                if v == code[1]:
                    return Card(k, code[0])

class Deck(object):
    """\
    A deck of cards typically consists of 52 cards.
    """
    SUITS = ('♥', '♦', '♣', '♠')
    VALUES = range(2, 15)
    
    def __init__(self, shuffle=True):
        """Prepare a deck of cards. Shuffle by default. """
        self._cards = []
        self.reset()
        if shuffle:
            self.shuffle()
    def reset(self):
        """Make a new complete deck of cards. Don't forget to shuffle! """
        for suit in Deck.SUITS:
            for value in Deck.VALUES:
                self._cards.append(Card(value, suit))
    def shuffle(self):
        """Shuffle the cards within the deck. """
        random.shuffle(self._cards)
    def deal(self, n):
        if len(self._cards) < n:
            raise ValueError("More cards requested than available in the deck.")
        return Hand([self._cards.pop(0) for i in xrange(n)])
    
    def __len__(self):
        return len(self._cards)

class Hand(list):
    def __str__(self):
        return ' '.join(str(i) for i in reversed(sorted(self)))
    
def unduple(tail):
    """Convert list of lists to list of items 
    >>> list(unduple([[1], [2], [3]]))
    [[1, 2, 3]]
    >>> list(unduple([[1], [2, 2], [3]]))
    [[1, 2, 3], [1, 2, 3]]
    >>>
    """
    if len(tail) > 1:
        # long tail
        for item in tail[0]:
            for resolved in unduple(tail[1:]):
                yield [item] + resolved
    elif len(tail) == 1:
        for item in tail[0]:
            yield [item]

def highest_of_suit(hand, suit):
    """\
    Return the highest card of the given suit.
    Return None if no card with the given suit is present.
    """
    for card in reversed(sorted(hand)):
        if card.suit == suit:
            return card
    # Explicitly return 
    return None

def all_suited(hand):
    """\
    Yield partial hands with the same suit.
    Highest cards will be returned first
    """
    for suit in set([card.suit for card in hand]):
        suited = [card for card in reversed(sorted(hand)) if card.suit == suit]
        if suited: yield suited

def filter_length(hands, number):
    """\
    Filter hands that are of at least the given number of cards.
    Only the first given number of cards will be let through per hand"""
    for hand in hands:
        if len(hand) >= number:
            yield hand[:number]

def filter_suited(hands):
    """Let through hands that are of the same suit """
    for hand in hands:
        if len(set([c.suit for c in hand])) == 1:
            yield hand

def filter_royal(hands):
    """Is the first card an Ace?
    Be careful with this check. It assumes a valid straight as input!
    """
    for hand in hands:
        if hand[0].value == 14:
            yield hand

def all_straights(hand, length=5):
    """\
    Return all possible straights of the given length.
    The highest possible straight will be returned first.
    """
    # copy a sorted version of the hand
    sorted_hand = list(reversed(sorted(hand)))
    # Append Aces to the end for 5432A straights
    for card in sorted_hand:
        if card.value == 14:
            sorted_hand.append(card)
        break
    straights = []
    tmp = []
    prev = None
    for card in sorted_hand:
        # New possible straight
        if not prev:
            prev = card
            tmp.append([card])
            continue
        # else
        if card.value == prev.value:
            tmp[-1].append(card)
            continue
        # else 
        # Does this next card add to a straight? The -12 is 2 minus A (14)
        if prev.value - card.value in (1, -12):
            tmp.append([card])
            if len(tmp) > length:
                straights.append(tmp[:length])
                tmp.pop(0)
        else:
            # Not a next card in straight
            if len(tmp) == length:
                # Yes, we built a straight
                straights.append(tmp)
            # Empty tmp straight
            tmp = [[card]]
        prev = card
    # Evaluate tmp when leaving the loop
    if len(tmp) == length:
        straights.append(tmp)
    # Convert straights with duplicate cards to multiple straights
    undupled = []
    for solution in straights:
        undupled.extend(unduple(solution))
    return reversed(sorted(undupled))

def first(sequence, n=1):
    result = []
    for i in sequence:
        result.append(i)
        if len(result) == n:
            return result
        
def weave(sequence):
    """Weave a sequence of 2 deep to 1 deep 
    >>> list(weave([[1, 2], [3, 4]]))
    [1, 2, 3, 4]
    >>> list(weave([[1, 2], [3, 4], [5, 6]]))
    [1, 2, 3, 4, 5, 6]
    >>> list(weave(islice([[1, 2], [3, 4], [5, 6]], 2)))
    [1, 2, 3, 4]
    >>>
    """
    for subseq in sequence:
        for item in subseq:
            yield item
            
def all_full_house(hand):
    """Return all possible full house combinations within given hand """
    for trip in all_of_a_kind(hand, 3):
        for doub in all_of_a_kind(hand, 2):
            yield trip + doub

def all_of_a_kind(hand, number):
    """\
    Return partial hands with the given number of occurences in hand.
    Highest set will be returned first.
    >>> hand = [Card(2, '♥'), Card(3, '♥'), Card(2, '♠'), Card(3, '♦'), Card(3, '♣')]
    >>> list(all_of_a_kind(hand, 2))
    [[Card(2, '♥'), Card(2, '♠')]]
    >>> list(all_of_a_kind(hand, 3))
    [[Card(3, '♥'), Card(3, '♦'), Card(3, '♣')]]
    """
    paired = {}
    for card in hand:
        paired.setdefault(card.value, []).append(card)
    for value in reversed(sorted(set([c.value for c in hand]))):
        if len(paired[value]) == number:
            yield paired[value]
    
class PokerValue(object):
    """\
    Determine the scoring of the given hand of cards following poker rules.
    
    Internally builds a tuple with True / False values for each possible combination
    """
    scorings = [
        # #cards needed for the combination, name, evaluation function
        (5, 'Royal flush', lambda x: first(filter_royal(filter_suited(all_straights(x))))),
        (5, 'Straight flush', lambda x: first(filter_suited(all_straights(x)))),
        (4, 'Four of a kind', lambda x: first(all_of_a_kind(x, 4))),
        (5, 'Full house', lambda x: first(all_full_house(x))),
        (5, 'Flush', lambda x: first(filter_length(all_suited(x), 5))),
        (5, 'Straight', lambda x: first(all_straights(x))),
        (3, 'Three of a kind', lambda x: first(all_of_a_kind(x, 3))),
        (4, 'Two pair', lambda x: [list(weave(islice(all_of_a_kind(x, 2), 2)))]),
        (2, 'One pair', lambda x: first(all_of_a_kind(x, 2))),
        (1, 'High card', lambda x: [[max(x)]]),
        (1, 'High card', lambda x: [[max(x)]]),
        (1, 'High card', lambda x: [[max(x)]]),
        (1, 'High card', lambda x: [[max(x)]]),
        (1, 'High card', lambda x: [[max(x)]]),
    ]
    def __init__(self, hand):
        """Evaluate hand """
        self._value = []
        # Helpers
        rest = Hand(hand)
        scoring_cards = []
        for (n, name, f) in PokerValue.scorings:
            if len(rest) < n:
                self._value.append(None)
                continue
            # else
            win = f(rest)
            if not win:
                self._value.append(None)
            else:
                # Pick the first 
                win = Hand(win[0])
                self._value.append(win)
                scoring_cards.extend(win)
                for card in win:
                    rest.remove(card)
            if not rest or len(scoring_cards)==5: break
    def __cmp__(self, other):
            """\
            >>> hand0 = Hand([Card(13, '♠')])
            >>> hand1 = Hand([Card(13, '♥')])
            >>> cmp(PokerValue(hand0), PokerValue(hand1))
            0
            >>> PokerValue(hand0) > PokerValue(hand1)
            False
            >>> PokerValue(hand0) < PokerValue(hand1)
            False
            >>> PokerValue(hand0) == PokerValue(hand1)
            True
            """
            return cmp(self._value, other._value)
    def __str__(self):
        result = []
        for (i, hand) in enumerate(self._value):
            if hand:
                result.append("%s with %s" % (PokerValue.scorings[i][1], hand))
        return '\n'.join(result)
    
def tests():
    """\
    Situaties die fout gingen:
    >>> errors = [\
            'sA hK dQ c9 d8 h7 d6 s3 s2',\
            'sA hK dQ cQ d7 h7 d6 h6 s2',\
        ]
    >>> for line in errors:
    ...     a = PokerValue(Hand([Card.from_string(c) for c in line.split()]))
    >>>
    """
    import doctest
    doctest.testmod(verbose=False)
    
def main():
    #tests()
    
    deck = Deck(shuffle=False)
    deck = deck.deal(52)
    
    hand = Hand(deck[:4] + [deck[12]] + deck[13:15] + deck[25:27])
    print hand
#    print PokerValue(hand)
    
    hand = Hand(deck[3:6] + deck[16:19])
    print hand
#    print PokerValue(hand)
    
    hand = Hand(deck[3:6] + deck[18:20])
    print hand
#    print PokerValue(hand)
    
    hand = Hand(deck[0:4] + deck[20:21])
    print hand
    print PokerValue(hand)
    
    return None
    deck = Deck()
    deck.shuffle()
    
    board = Hand(deck.deal(5))
    hands = [Hand(deck.deal(2)) for i in xrange(2)]
    print board
    for (i, hand) in enumerate(hands):
        print i, hand, PokerValue(hand + board)
    
if __name__ == "__main__":
    main()